﻿// (c) Copyright 2013 Mike Riedel.
// This source is subject to the Microsoft Public License (Ms-PL).
// All other rights reserved.

using System;
using System.Linq;
using FreshMeat.Storage;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Storage.More.Test
{
   public abstract class CommonRepositoryTest
   {
      [TestMethod]
      public void SaveDoesNotModifyInstance()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = new Guid("c271739d941a40ecbd506e029f59d230") };

         repo.Save(item);

         Assert.AreEqual(1, item.Id, "Id is wrong.");
         Assert.AreEqual("One", item.Name, "Name is wrong.");
         Assert.AreEqual(new Guid("c271739d941a40ecbd506e029f59d230"), item.UniqueId, "UniqueId is wrong.");
      }

      [TestMethod]
      public void SaveIndicatesPriorExistence()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };

         var result1 = repo.Save(item);
         var result2 = repo.Save(item);
         var result3 = repo.Save(item);

         Assert.IsFalse(result1, "First save should return false because item didn't already exist.");
         Assert.IsTrue(result2, "Second save should return true because item already existed.");
         Assert.IsTrue(result3, "Third save should return true because item already existed.");
      }

      [TestMethod]
      public void SaveManyIndicatesPriorExistence()
      {
         var repo = CreateRepository();
         var item1 = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         var item2 = new Item { Id = 2, Name = "Two", UniqueId = Guid.NewGuid() };
         var item3 = new Item { Id = 3, Name = "Three", UniqueId = Guid.NewGuid() };
         var item4 = new Item { Id = 4, Name = "Four", UniqueId = Guid.NewGuid() };
         var item5 = new Item { Id = 5, Name = "Five", UniqueId = Guid.NewGuid() };

         var count1 = repo.Save(new[] {item1, item2, item3, item4});
         var count2 = repo.Save(new[] {item4, item2, item3, item1});
         var count3 = repo.Save(new[] {item5, item4, item3, item2});

         Assert.AreEqual(0, count1, "First save should have reported 0 pre-existing items.");
         Assert.AreEqual(4, count2, "Second save should have reported 4 pre-existing items.");
         Assert.AreEqual(3, count3, "Third save should have reported 3 pre-existing items.");
      }

      [TestMethod]
      public void SaveUpdatesPreviousSave()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };

         repo.Save(item);
         item.Name = "OneAgain";
         repo.Save(item);
         var loaded = repo.Load(item.Id);

         Assert.AreEqual(1, item.Id, "Id should not have changed.");
         Assert.AreEqual("OneAgain", item.Name, "Name should have changed.");
         Assert.AreEqual(item.UniqueId, loaded.UniqueId, "UniqueId should not have changed.");
      }

      [TestMethod]
      public void LoadedInstanceIsNotSavedInstance()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         
         repo.Save(item);
         var loaded = repo.Load(item.Id);

         Assert.AreNotSame(item, loaded, "Loaded instance cannot match saved instance.");
      }

      [TestMethod]
      public void DifferentInstanceWithEveryLoad()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         
         repo.Save(item);
         var loaded1 = repo.Load(item.Id);
         var loaded2 = repo.Load(item.Id);

         Assert.AreNotSame(loaded1, loaded2, "Two loaded instances cannot be the same.");
      }

      [TestMethod]
      public void LoadedItemMatchesSavedItem()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         repo.Save(item);

         var loaded = repo.Load(item.Id);

         Assert.AreEqual(item.Id, loaded.Id, "Id is wrong.");
         Assert.AreEqual(item.Name, loaded.Name, "Name is wrong.");
         Assert.AreEqual(item.UniqueId, loaded.UniqueId, "UniqueId is wrong.");
      }

      [TestMethod]
      [ExpectedException(typeof(NotFoundException), "Should have thrown exception because requested item did not exist.")]
      public void LoadThrowsWhenNotFound()
      {
         var repo = CreateRepository();

         repo.Load(100);
      }

      [TestMethod]
      public void LoadUsesFactoryWhenNotFound()
      {
         var repo = CreateRepository();

         var item = repo.Load(100, id => new Item {Id = id});

         Assert.IsNotNull(item, "Load should have used factory to create and return a new item.");
         Assert.AreEqual(100, item.Id, "Created item has wrong Id.");
      }

      [TestMethod]
      public void LoadManyCreatesWhenNotFound()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 2, Name = "Two", UniqueId = Guid.NewGuid() };
         repo.Save(item);

         var result = repo.Load(new[] { 1, 2, 3 }, id => new Item { Id = id }).ToArray();

         Assert.AreEqual(3, result.Length, "Load should have returned 3 items (1 existing and 2 missing).");
         Assert.AreEqual(1, result[0].Id, "Item[0] has wrong Id.");
         Assert.AreEqual(2, result[1].Id, "Item[1] has wrong Id.");
         Assert.AreEqual(3, result[2].Id, "Item[2] has wrong Id.");
      }

      [TestMethod]
      [ExpectedException(typeof(NotFoundException), "Should have thrown exception because at least one requested item did not exist.")]
      public void LoadManyThrowsWhenNotFound()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 2, Name = "Two", UniqueId = Guid.NewGuid() };
         repo.Save(item);

         repo.Load(new[] { 1, 2, 3 });
      }

      [TestMethod]
      public void LoadAllReturnsNothing()
      {
         var repo = CreateRepository();

         var result = repo.Load();

         Assert.IsNotNull(result, "Load should not return null.");
         Assert.AreEqual(0, result.Count(), "Load should return empty enumerable when there are no items.");
      }

      [TestMethod]
      public void LoadAllReturnsEverything()
      {
         var repo = CreateRepository();
         var item1 = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         var item2 = new Item { Id = 2, Name = "Two", UniqueId = Guid.NewGuid() };
         var item3 = new Item { Id = 3, Name = "Three", UniqueId = Guid.NewGuid() };
         var item4 = new Item { Id = 4, Name = "Four", UniqueId = Guid.NewGuid() };
         repo.Save(new[] { item1, item2, item3, item4 });

         var result = repo.Load();

         Assert.IsNotNull(result, "Load should not return null.");
         Assert.AreEqual(4, result.Count(), "Load should return all available items.");
      }

      [TestMethod]
      public void DeleteIndicatesPriorExistence()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };

         var result1 = repo.Delete(1);
         repo.Save(item);
         var result2 = repo.Delete(1);
         var result3 = repo.Delete(1);

         Assert.IsFalse(result1, "First delete should return false because item didn't exist.");
         Assert.IsTrue(result2, "Second delete should return true because item did exist.");
         Assert.IsFalse(result3, "Third delete should return false because item no longer exists.");
      }

      [TestMethod]
      public void DeleteManyIndicatesPriorExistence()
      {
         var repo = CreateRepository();
         var item1 = new Item {Id = 1, Name = "One", UniqueId = Guid.NewGuid()};
         var item2 = new Item {Id = 2, Name = "Two", UniqueId = Guid.NewGuid()};
         var item3 = new Item {Id = 3, Name = "Three", UniqueId = Guid.NewGuid()};
         var item4 = new Item {Id = 4, Name = "Four", UniqueId = Guid.NewGuid()};
         var item5 = new Item {Id = 5, Name = "Five", UniqueId = Guid.NewGuid()};
         repo.Save(new[] { item1, item2, item3, item4, item5 });

         var count1 = repo.Delete(new[] {1, 3});
         var count2 = repo.Delete(new[] {1, 2, 3});
         var count3 = repo.Delete(new[] {1, 2, 3, 4, 5});

         Assert.AreEqual(2, count1, "First delete should have reported 2 actual deletes.");
         Assert.AreEqual(1, count2, "Second delete should have reported 1 actual delete.");
         Assert.AreEqual(2, count3, "Third delete should have reported 2 actual deletes.");
      }

      [TestMethod]
      [ExpectedException(typeof(NotFoundException))]
      public void DeletePreventsLoad()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         repo.Save(item);

         repo.Delete(1);
         repo.Load(1); // should throw exception since item should no longer exist
      }

      [TestMethod]
      public void DeleteMayBeDelayed()
      {
         var repo = CreateRepository();

         for (var i = 0; i < 100; i++)
         {
            var item = new Item {Id = i, Name = "blah" + i, UniqueId = Guid.NewGuid()};
            repo.Save(item);
         }

         for (var i = 0; i < 100; i++)
         {
            repo.Delete(i);
            try
            {
               repo.Load(i); // should throw exception since item should no longer exist
            }
            catch (NotFoundException)
            {
               // Expected exception -- everything is fine!
            }
            catch (Exception)
            {
               Assert.Fail("Got unexpected exception.");
            }
         }
      }

      [TestMethod]
      public void SaveWorksNormallyAfterDelete()
      {
         var repo = CreateRepository();
         var item = new Item { Id = 1, Name = "One", UniqueId = Guid.NewGuid() };
         repo.Save(item);

         repo.Delete(1);
         var result = repo.Save(item);

         Assert.IsFalse(result, "Save should have returned false to indicate item did NOT exist.");
      }

      /// <summary>
      /// When implemented in a derived class, returns a fresh repository instance.
      /// </summary>
      /// <returns></returns>
      protected abstract IRepository<int, Item> CreateRepository();
   }
}